﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.IO;


namespace BerkelyAlgorithem2
{
    public class Master
    {
        public NeighbourTable Neighbours;
        RunningClock clock;

        tcpClient[] clients;
        tcpServer[] servers;

        public int offsetsum;
        public int offsetCounter;
        public int bestOffset;
        public int worstOffset;
        public string bestOffseter;
        public string worstOffseter;
        public int cutoff;
        public double percent_Cutoff;
        public const int ABSOLUT_CUTOFF = 0;
        public const int PERCENT_CUTOFF = 1;
        public int cutoff_method = 0;
        public int timeBetweenSync = 0;

        
        public int allNupuSelection = 1;
        
        public const int takeMasterTime = 0;
        public const int takeAvgTime = 1;

        public bool masterNupaTooMuch = false;
        public int masterNupaCounter;
        public int masterNupaCutoff;

        System.Threading.Thread[] serverThreads;
        int threadCount = 0;
        Neighbour self;

        
        public Master(NeighbourTable ntb, RunningClock c)
        {
            offsetsum = 0;
            offsetCounter = 0;
            bestOffset = int.MaxValue;
            worstOffset = 0; ;
            bestOffseter = "";
            worstOffseter = "";

            cutoff = 400; // 400ms
            percent_Cutoff = 1; //100%

            masterNupaCounter = 0;
            masterNupaCutoff = 10;

            self=null;
            //Console.WriteLine("starting constructor of master");
            this.Neighbours = ntb;
            startFresh();
            //Console.WriteLine("\tcreating new RunningClock");

            this.clock = c;

            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.Address.Equals(utils.FindLanAddress()))
                {
                    self = n;
                }
            }

            //init tcpServer threads (listeners)
            //Console.WriteLine("\tcreating new thread array (serverThreads)");
            serverThreads = new System.Threading.Thread[Neighbours.Neighbours.Count - 1]; //-1 because Master also appears in the table

            //populate the 'clients' with IPs and Ports
            //Console.WriteLine("\tcreating new client");

            clients = new tcpClient[Neighbours.Neighbours.Count - 1];
            servers = new tcpServer[Neighbours.Neighbours.Count - 1];

            int i = 0;
            //Console.WriteLine("\tsatrting foreach of constructor");
            //System.Threading.Thread.Sleep(1000);
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive)
                {
                    if (!n.Address.Equals(utils.FindLanAddress()))
                    {
                        //Console.WriteLine("\t\tin if of foreach, creating new tcpClient");

                        try
                        {
                            if (n.IsAlive)
                                clients[i] = new tcpClient(n.Address, n.syncPort + Neighbours.Neighbours.Count);
                        }
                        catch
                        {
                            //i was unable to connect to a certain client, he's presumably MIA
                            n.IsAlive = false;
                        }
                        
                        try
                        {
                            if (n.IsAlive)
                                servers[i] = new tcpServer(IPAddress.Any, n.syncPort, n);
                        }
                        catch
                        {
                            //i was unable to open a port for this client, my ports are fucked up?

                            //i am a bad master, someone else should be master, and i should DIE
                            //throw ElectionsStarted
                            //Console.WriteLine("********************************");
                            //Console.WriteLine("I AM A BAD MASTER AND CAN'T OPEN PORTS!!!");
                            //Console.WriteLine("********************************");

                            Environment.Exit(0);
                        }
                        i++;
                    }
                }
            }
            //Console.WriteLine("\tending constructor");
            //System.GC.Collect();
            //GC.WaitForPendingFinalizers();
        }


        ~Master()
        {
            foreach (tcpClient c in clients)
            {
                if (c != null)
                {
                    c.exit();
                }
            }
            foreach (tcpServer s in servers)
            {
                if (s != null)
                {
                    s.exit();
                }
            }
            foreach (System.Threading.Thread t in serverThreads)
            {
                if (t != null)
                {
                    try
                    {
                        t.Abort();

                    }
                    catch
                    {

                    }
                }
            }
            System.GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        //implements all of the master's logic
        public void start()
        {

            System.Threading.Thread.Sleep(timeBetweenSync);
            //Console.WriteLine("starting start function\ncalling pollSlaves()");
            //poll slaves


            pollSlaves();

            //Console.WriteLine("returned from pollSlaves()\ncalling avarageTheClocks()");
            //average the clock times, ignoring any values it receives far outside the values of the others

            avarageTheClocks();

            //Console.WriteLine("returnd from avarageTheClocks()\n calling sendOffsetsToSlaves()");

            //send out the amount (positive or negative) that each slave must adjust its clock
            sendOffsetsToSlaves();
            
            //Console.WriteLine("returnd from sendOffsetsToSlaves()");
            //Console.WriteLine("ending start");
            
            //System.GC.Collect();
            //GC.WaitForPendingFinalizers();

            //print results to file
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                Console.Write(n.Address.ToString()+"\t");
            }
            Console.Write("\n");

            foreach (Neighbour n in Neighbours.Neighbours)
            {
                Console.Write(n.offset.ToString()+"\t");

            }
            Console.Write("\n");


        }

        private void sendOffsetsToSlaves()
        {
            //Console.WriteLine("starting sendOffsetsToSlaves()\n\tstarting first foreach");
            foreach (tcpClient c in clients)
            {
                //Console.WriteLine("\tstarting second foreach");
                foreach (Neighbour n in Neighbours.Neighbours)
                {
                    if (c!=null && n.Address.Equals(c.IP) && n.IsAlive && !n.Address.Equals(utils.FindLanAddress()))
                    {
                        if (c.tcpclnt.Connected)
                        {
                            //c.exit();
                            //c.open(n.Address, n.syncPort);

                            //prints
                            //Console.WriteLine("===========================");
                            //Console.WriteLine("slaves offset:" + n.offset);
                            //Console.WriteLine("===========================");

                            //Console.WriteLine("\tsending msg: " + n.offset.ToString() + " to " + c.IP.ToString() + ":" + c.port);
                            try
                            {

                                c.sendMsg((n.offset).ToString());
                                
                            }
                            catch
                            {
                                //unable to send an offset to a slave, he's persumably MIA...
                                n.IsAlive = false;
                            }
                        }
                        break;
                    }
                }
            }

            //System.GC.Collect();
            //GC.WaitForPendingFinalizers();
        }

        private void avarageTheClocks()
        {
            //Console.WriteLine("starting avarageTheClocks()");
            long sum = 0;
            int countSlaves = Neighbours.Neighbours.Count;

            //update my clock & sum's
            long myClockInsMS = utils.getTimeInMS((utils.DateTimeAsString(clock.CurrentTime).Split(':')));
            
            //start master RTT! (TKURA)
            self.RTToldTime = DateTime.Now;
            
            //Console.WriteLine("\tMaster's time before balagan=\t" + utils.DateTimeAsString(clock.CurrentTime));
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.Address.Equals(utils.FindLanAddress()))
                {
                    n.timeInMS=myClockInsMS;
                    n.nupa = false;
                }
                
                //calc tot time sum
                if (n.IsAlive)
                {
                    sum += n.timeInMS;
                    n.nupa = false;
                }
                else
                {
                    //this slave is dead, dont count him
                    countSlaves--;
                }
            }

            //Console.WriteLine("\tSum of all neighbours(includng me) =" + sum);
          
            //Console.WriteLine("\tmyClockInsMS=\t" + myClockInsMS);


            //calc the avg of all
            
            double currAvg = sum / countSlaves;
            
            //Console.WriteLine("\tcurrAvg =\t" + currAvg);
            
            //see who has a fucked up offset from AVG, using two diffrent methods
            double goodAvg=0;

                if (cutoff_method == ABSOLUT_CUTOFF)
                {
                    goodAvg = absolut_Cutoff_Method(currAvg, sum);
                }
                else if (cutoff_method == PERCENT_CUTOFF)
                {
                    goodAvg = percent_Cutoff_Method(currAvg, sum);
                }

            //if the master was 'NUPA' 10 times consecutive
            if (masterNupaCounter > masterNupaCutoff)
            {
                masterNupaTooMuch = true;
            }
            //Console.WriteLine("\tgoodAvg=\t" + goodAvg);
            
            //update MY neighbour table with their offsets
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive)
                {
                    n.offset = (int)((long)goodAvg - n.timeInMS);
                    //Console.WriteLine("\tslave " + n.Address.ToString() + " time should be: " + utils.TimeInMSAsString(n.timeInMS + n.offset));

                    offsetsum += n.offset;
                    offsetCounter++;
                    
                    if (Math.Abs(n.offset) < Math.Abs(bestOffset))
                    {
                        bestOffset = n.offset;
                        bestOffseter = n.Address.ToString();
                    }
                    
                    if (Math.Abs(n.offset) > Math.Abs(worstOffset))
                    {
                        worstOffset = n.offset;
                        worstOffseter = n.Address.ToString();
                    }
                }
                //offsetCounter--;//sub the master
                //fix my (masters) time with the AVG
                if (n.Address.ToString().Equals(self.Address.ToString()))
                {
                    self.RTTnewTime = DateTime.Now;
                    self.RTT = (int)utils.subDateTimesGetTotMS(self.RTTnewTime, self.RTToldTime); //tkura

                    //Console.WriteLine("TKURA OF MASKER WAS: " + self.RTT);

                    //Console.WriteLine("\tgoodTime's TimeInMSAsString=\t" + utils.TimeInMSAsString((long)goodAvg));

                    //clock.CurrentTime = utils.StringAsDateTime( utils.TimeInMSAsString((long)goodAvg+self.RTT)  ,DateTime.Now);
                    clock.setTime(self.offset + self.RTT);

                    //Console.WriteLine("\tMaster's time after set=\t" + utils.DateTimeAsString(clock.CurrentTime));
                }
            }


            //System.GC.Collect();
            //GC.WaitForPendingFinalizers();
        }

        private double percent_Cutoff_Method(double currAvg, long sum)
        {
            int i = 0;
            long offsetSum = 0;
            long goodSum = 0;
            double goodAvg = 0;

            //calc offsets
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive && !n.nupa)
                {
                    i++;
                    offsetSum += (long)Math.Abs(currAvg - (n.timeInMS));
                }
            }

            double offsetAVG = offsetsum / i;

            i = 0;
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive && !n.nupa)
                {
                    if (System.Math.Abs((currAvg - n.timeInMS)) <= offsetAVG * (percent_Cutoff))
                    {
                        goodSum += n.timeInMS;
                        i++;

                        //if the master didnt 'NUPA'
                        if (n.Address.Equals(utils.FindLanAddress()))
                        {
                            masterNupaCounter=0;
                            this.masterNupaTooMuch = false;
                        }
                        n.nupaCounter = 0;
                    }
                    else
                    {
                        n.nupaCounter++;
                        //if the master 'NUPA'
                        if (n.Address.Equals(utils.FindLanAddress()))
                        {
                            masterNupaCounter++;
                        }
                        ////System.//Console.WriteLine("slave " + n.Address.ToString() + " was nupa");
                    }
                }
            }

            //Console.WriteLine("\tnewSum=\t" + goodSum);

            //THIS IS THE CASE WHERE WE ALL NIPINU OURSELVES!!!
            if (goodSum == 0)
            {
                if (allNupuSelection == takeMasterTime)
                {
                    goodSum = self.timeInMS;
                    i = 1;
                }
                else //take the avg
                {
                    goodSum = (long)currAvg;
                    i = 1;
                }
            }


            //re-avg
            goodAvg = goodSum / i;


            return goodAvg;

        }

        public double absolut_Cutoff_Method(double currAvg, long sum)
        {
            //Console.WriteLine("\tsee who has a fucked up offset from AVG, cutoff="+cutoff+" ms diffrence");
            int i = 0;
            long newSum = 0;

            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive && !n.nupa)
                {
                    if (System.Math.Abs((currAvg - n.timeInMS)) < cutoff)
                    {
                        //Console.WriteLine("\tin foreach in if,sum = " + sum + " slave = " + n.Address.ToString());
                        newSum += n.timeInMS;
                        i++;

                        //if the master didnt 'NUPA'
                        if (n.Address.Equals(utils.FindLanAddress()))
                        {
                            masterNupaCounter = 0;
                            this.masterNupaTooMuch = false;
                        }
                        n.nupaCounter = 0;
                    }
                    else
                    {
                        n.nupaCounter++;
                        //if the master 'NUPA'
                        if (n.Address.Equals(utils.FindLanAddress()))
                        {
                            masterNupaCounter++;
                        }
                        ////System.//Console.WriteLine("slave " + n.Address.ToString() + " was nupa");
                    }
                }
            }

            //Console.WriteLine("\tnewSum=\t" + newSum);

            //THIS IS THE CASE WHERE WE ALL NIPINU OURSELVES!!! TOFIX!!!!
            if (newSum == 0)
            {
                if (allNupuSelection == takeMasterTime)
                {
                    newSum = self.timeInMS;
                    i = 1;
                }
                else //take the avg
                {
                    newSum = (long)currAvg;
                    i = 1;
                }

            }


            //re-avg
            double goodAvg = newSum / i;

            //THIS IS THE CASE WHERE THE MASTER NIPA HIMSELF
            if (System.Math.Abs((currAvg - self.timeInMS)) >= cutoff)
            {
                // take the GoodAvg, done later in the code.
            }

            return goodAvg;
        }

        public long FindMedian() 
        {
            long[] times = new long[Neighbours.Neighbours.Count];
            int i = 0;

            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive)
                {
                    times[i] = n.timeInMS;
                    i++;
                }
            }          
            Array.Sort(times);
           
            return times[i / 2];

        }
        //public double optimizedMode(double currAvg)
        //{
        //    int inAvg = 0;
        //    int totalAlive = 0;
        //    long newSum = 0;
        //    long offsetSum = 0;



        //    while (true)
        //    {
        //        totalAlive = 0;
        //        newSum = 0;
        //        inAvg = 0;

        //        //part 1 - absolut cutoff
        //        foreach (Neighbour n in Neighbours.Neighbours)
        //        {
        //            if (n.IsAlive && !n.nupa)
        //            {
        //                totalAlive++;
        //                if (cutoff_method == ABSOLUT_CUTOFF)
        //                {
        //                    //abs cutoff method
        //                    if (System.Math.Abs((currAvg - n.timeInMS)) < cutoff)
        //                    {
        //                        newSum += n.timeInMS;
        //                        inAvg++;

        //                        //if the master didnt 'NUPA'
        //                        if (n.Address.Equals(utils.FindLanAddress()))
        //                        {
        //                            masterNupaCounter = 0;
        //                            this.masterNupaTooMuch = false;
        //                        }
        //                    }
        //                    else
        //                    {
        //                        //if the master 'NUPA'
        //                        if (n.Address.Equals(utils.FindLanAddress()))
        //                        {
        //                            masterNupaCounter++;
        //                        }
        //                        ////System.//Console.WriteLine("slave " + n.Address.ToString() + " was nupa");
        //                    }
        //                }
        //                else
        //                {
        //                    //percent cutoff method
        //                    offsetSum=0;
        //                    totalAlive = 0;
        //                    newSum = 0;
        //                    inAvg = 0;

        //                    //calc offsets
        //                    foreach (Neighbour ne in Neighbours.Neighbours)
        //                    {
        //                        if (ne.IsAlive && !ne.nupa)
        //                        {
        //                            inAvg++;
        //                            offsetSum += (long)Math.Abs(currAvg - (ne.timeInMS));
        //                        }
        //                    }

        //                    double offsetAVG = offsetsum / inAvg;

        //                    if (System.Math.Abs((currAvg - n.timeInMS)) <= offsetAVG * (percent_Cutoff))
        //                    {
        //                        newSum += n.timeInMS;
        //                        inAvg++;

        //                        //if the master didnt 'NUPA'
        //                        if (n.Address.Equals(utils.FindLanAddress()))
        //                        {
        //                            masterNupaCounter = 0;
        //                            this.masterNupaTooMuch = false;
        //                        }
        //                    }
        //                    else
        //                    {
        //                        //if the master 'NUPA'
        //                        if (n.Address.Equals(utils.FindLanAddress()))
        //                        {
        //                            masterNupaCounter++;
        //                        }
        //                        ////System.//Console.WriteLine("slave " + n.Address.ToString() + " was nupa");
        //                    }
        //                }
        //            }
        //        }

        //        //if no one was 'NUPA'
        //        if (inAvg == totalAlive)
        //        {
        //            return newSum / inAvg;
        //        }


        //        //remove most far from avg
        //        Neighbour nupa = farest_from_avg(newSum / inAvg);

        //        if (nupa == null)
        //        {
        //            //all is well, and not interseting...
        //            return newSum / inAvg;
        //        }

        //    }

        //}

        private Neighbour farest_from_avg(long avg)
        {
            double d = 0;
            Neighbour b=null;
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive && !n.nupa)
                {
   
                    if (Math.Abs(avg - n.timeInMS) > d)
                    {   
                        
                        d = Math.Abs(avg - n.timeInMS);
                        b = n;
                          
                    }
                }
            }
            if (b != null)
            {
                b.nupa = true;
            }

            return b;
        }
        //updates the Neighbour's RTT and Time values
        public void pollSlaves()
        {
            //Console.WriteLine("starting pollSlaves()");
            //send the messages

            foreach (tcpClient c in clients)
            {
                foreach (Neighbour n in Neighbours.Neighbours)
                {
                    if(c!=null)
                    {
                        if (n.Address.Equals(c.IP))
                        {

                            if (n.IsAlive)
                            {
                                if (!n.Address.Equals(utils.FindLanAddress()))
                                {
                                    //Console.WriteLine("\tsending to " + c.IP + " msg: \"!!!\"");

                                    try
                                    {
                                        c.sendMsg("!!!"); //"secret" message to poll times from slaves
                                    }
                                    catch
                                    {
                                        //was unable to send msg, slave is MIA
                                        n.IsAlive = false;
                                        //Console.WriteLine("\t" + n.Address.ToString() + " has died, and now not alive");
                                        break;
                                    }

                                    if (n.IsAlive)
                                    {
                                        //Console.WriteLine("\tstarting RTT calculation...");
                                        n.RTToldTime = DateTime.Now;
                                    }
                                }

                            }
                        }
                    }
                }
            }
            
            //listen for results
            int i = 0;
            //Console.WriteLine("\tlistening for results");
            foreach (Neighbour n in Neighbours.Neighbours)
            {
                if (n.IsAlive)
                {
                    if (!n.Address.ToString().Equals(utils.FindLanAddress().ToString()))
                    {
                        serverThreads[i] = new System.Threading.Thread(() => listen(n));
                        //serverThreads[i].IsBackground = true;
                        serverThreads[i].Start();
                        i++;
                    }
                }
            }

            //now loop until all threads returned

            //Console.WriteLine("\tloop until all threads returned");
            bool flag=true;
            while (flag)
            {
                flag = false;
                foreach (System.Threading.Thread t in serverThreads)
                {
                    if (t!=null && t.IsAlive)
                    {
                        flag = true;
                        break;
                    }

                }
            }
            //Console.WriteLine("\tAll listening threads have returened");
            //Console.WriteLine("\tending pollSlaves()");
            
            //System.GC.Collect();
            //GC.WaitForPendingFinalizers();
        }

        public void listen(Neighbour n)
        {
            string fileName = "thread " + n.Address.ToString() + threadCount;
            string text = "";

            text += "starting listen()\n";
            tcpServer ser = null;
            Neighbour tempNeighbour=null;
            byte[] msg = null;
            foreach (Neighbour ne in Neighbours.Neighbours)
            {
                if (n.IsAlive)
                {
                    foreach (tcpServer s in servers)
                    {
                        if (s != null)
                        {
                            if (s.ident.Address.Equals(n.Address))
                            {
                                tempNeighbour = n;
                                ser = s;
                            }
                        }
                    }
                }
            }

            try
            {
                msg = ser.getMsg();
                if (msg != null && msg.Length != 0)
                    n.RTTnewTime = DateTime.Now;
                else
                {
                    tempNeighbour.IsAlive = false;
                    return;
                }


            }
            catch
            {
               //text += "no server existing for client: " + n.Address + "\n";
                tempNeighbour.IsAlive = false;
                //return;
            }


            if (tempNeighbour.IsAlive)
            {
                text += "\n\tgot slave's time: " + System.Text.Encoding.UTF8.GetString(msg) + "\n";

                n.RTT = (int)utils.subDateTimesGetTotMS(n.RTTnewTime, n.RTToldTime);

                text += "\nSlaves RTT was:" + n.RTT + "\n";

                text += "\treseting stop-watch\n";

                text += "\t" + n.Address + "`s RTT is " + n.RTT + "\n";

                long hisTimeInMS;
                string str = utils.ByteToStrArray(msg);
                try
                {

                    hisTimeInMS = utils.getTimeInMS((utils.DateTimeAsString(utils.StringAsDateTime(str, clock.CurrentTime)).Split(':')));

                    text += "\thisTimeInMS " + hisTimeInMS + "\n";

                    n.timeInMS = hisTimeInMS;

                    //deduct 0.5*RTT from his time
                    n.timeInMS -= n.RTT / 2;

                    n.time = utils.StringAsDateTime(str, clock.CurrentTime);

                    text += "\t" + n.Address + "s old time is " + n.time + "\n";

                    text += "RTT startTime was: " + utils.DateTimeAsString(n.RTToldTime) + "\nRTT endTime was: " + utils.DateTimeAsString(n.RTTnewTime) + "\n";

                    //System.IO.File.WriteAllText(@"C:\Users\ASoliD\Desktop\" + fileName, text);
                    threadCount++;
                }
                catch
                {
                    n.IsAlive = false;
                    return;
                }

                //System.GC.Collect();
                //GC.WaitForPendingFinalizers();
            }
        }


        private void startFresh()
        {
            foreach (Neighbour ne in Neighbours.Neighbours)
            {
                ne.IsAlive = true;
            }
        }

    }
}
